 #! /usr/bin/env python

import os
import time
import random
import math
import copy
import logging
from utils import singleton

@singleton
class Scheduler():
    """ Get scheduler INFO from the dumpers, 
        and do pooling operations for VMs dynamically.
    """
    version = 0.1

    #           sc_ring_info:  A VM is network sensitive or not :(domid, sensitive)
    #                          ie. {1:1, 2:0, 3:0, 4:0, 5:1, 6:0}
    #           sc_load_info:  Load of each VM : (domid, load)
    #                          ie. {1:0.3, 2:1.2, 3:1.0, 4:1.4, 5:0.5, 6:0.8}
    #          sc_sched_info:  A VM is in pool_0 or pool_1, including before and after scheduling.
    #                          ie. [{1:1, 2:0, 3:0, 4:0, 5:1, 6:0}, {1:0, 2:1, 3:0, 4:1, 5:0, 6:0}]
    #           sc_pool_info:  Cpu list of two pools
    #                          ie. [[0, 1, 2, 3, 4, 5], []]
    #                  sc_mu:  A decimal less than 1.0, but very close to 1.0
    #                          (mu * num_cpu_pool_i) as a threshold to tell a pool is hunger or not
    #                          ie. 0.99
    #              sc_num_vm:  Number of current VMs
    #             sc_num_cpu:  Number of physical CPUs
    #   sc_real_pool_num_cpu:  Number of PCPU a pool is need in reality, a float number.
    #                          ie. [3.2, 1.8]
    #    sc_int_pool_num_cpu:  Number of cpus of each pool after scheduling
    #                          ie. [3, 2]
    def __init__(self, num_cpu):

        #
        logging.basicConfig(
                level = logging.DEBUG,
                format = '[%(asctime)s] %(name)s:%(levelname)s: %(message)s'
        )
        logging.disable(logging.DEBUG)
        
        # Input: static 
        self.sc_mu = 0.98
        self.sc_num_cpu = num_cpu[0]     # number of total physical cpu

        # Input: update periodically
        self.sc_ring_info = {}
        self.sc_load_info = {}

        # Output: initilized and compute after that 
        self.sc_sched_info = [{}, {}]   # 
        self.sc_pool_info = [[], []]    #
        self.sc_num_vm = 0
        self.sc_hunger_info = {}

        # Metrics
        self.sc_real_pool_num_cpu = [0.0, 0.0]  # float number of pool-0 and pool-1
        self.sc_int_pool_num_cpu = [0, 0]   # integer number of pool-0 and pool-1 

    # Initialize pool
    def init_pools(self, pool_vm_lists, pool_cpu_lists):
        self.sc_sched_info = copy.deepcopy(pool_vm_lists)
        self.sc_pool_info = copy.deepcopy(pool_cpu_lists)
        #if self.sc_pool_info[0].size() + self.sc_pool_info[1].size()

    def version(self):
        print version
        return version

    # Update schedule parameters
    def update(self, ring_info, load_info):
        self.sc_ring_info = copy.deepcopy(ring_info)
        self.sc_load_info = copy.deepcopy(load_info)
        self.sc_num_vm = len(ring_info)

    # Output real_pool_cpu_num and int_pool_cpu_num
    def get_cpu_metric(self):
        ret = []
        ret.append(self.sc_real_pool_num_cpu)
        ret.append(self.sc_int_pool_num_cpu)
        return ret

    # domid:[load, ring, poolid, hunger]
    def get_domain_info(self):
        ret = {}

        # load
        for domid in self.sc_load_info.keys():
            ret[domid] = []
            ret[domid].append(self.sc_load_info[domid])
        # 0, 1
        if self.sc_ring_info:
            for domid in self.sc_ring_info.keys():
                ret[domid].append(self.sc_ring_info[domid])
        else:
            for domid in self.sc_load_info.keys():
                ret[domid].append(0)
        # pool id
        for domid in self.sc_sched_info[1].keys():
            ret[domid].append(self.sc_sched_info[1][domid])
        # 0, 1
        # empty at first, needing to take care
        if self.sc_hunger_info:
            for domid in self.sc_hunger_info.keys():
                ret[domid].append(self.sc_hunger_info[domid])
        else :
            for domid in self.sc_load_info.keys():
                ret[domid].append(0)

        logging.debug(str(ret))
        return ret

    # Output CPU change of each pool
    def get_cpu_list(self):
        return self.sc_pool_info

    # Output VM change of each pool
    def get_vm_list(self):
        return self.sc_sched_info[0]

    # Compute scheduling parameters and do pooling operations
    def dispatch(self):
        # rm dom0's load info 
        remain_cpus = self.sc_num_cpu - self.sc_load_info[0]
        del self.sc_load_info[0]
        
        # The VM is hunger(1) or not(0)
        hunger_vms = {}
        for domid in self.sc_load_info.keys():
            hunger_vms[domid] = 0

        # 1. Get load of each pool
        load_pool_0 = 0.0
        load_pool_1 = 0.0
        num_vm_pool_0 = 0
        num_vm_pool_1 = 0
        for domid in self.sc_load_info.keys(): # How about a new domain not in sc_sched_info
            if (domid in self.sc_sched_info[0].keys()) and (self.sc_sched_info[0][domid] == 0):
                load_pool_0 += self.sc_load_info[domid]
                num_vm_pool_0 += 1
            if (domid in self.sc_sched_info[0].keys()) and (self.sc_sched_info[0][domid] == 1):
                load_pool_1 += self.sc_load_info[domid]
                num_vm_pool_1 += 1

        # 2. Compute hunger VMs
        # 2.1. Average cpu_num of each pool if the load of each VM is equal
        avg_cpu_num_pool_0 = 0.0
        avg_cpu_num_pool_1 = 0.0
        avg_cpu_num_pool_0 = self.sc_int_pool_num_cpu[0]/num_vm_pool_0 if num_vm_pool_0 > 0 else 0
        avg_cpu_num_pool_1 = self.sc_int_pool_num_cpu[1]/num_vm_pool_1 if num_vm_pool_1 > 0 else 0

        # 2.2. The VM is hunger ?
        hunger_pool_0 = 0
        hunger_pool_1 = 0
        sensitive_num = 0 # number of sensitive VMs
        for domid in self.sc_load_info.keys():
            # If not new domain
            if domid in self.sc_sched_info[0].keys():
                if (load_pool_0 >= self.sc_int_pool_num_cpu[0]*self.sc_mu)\
                    and (self.sc_sched_info[0][domid] == 0\
                    and self.sc_load_info[domid] > avg_cpu_num_pool_0):
                    hunger_vms[domid] = 1
                    hunger_pool_0 += 1
                if (load_pool_1 >= self.sc_int_pool_num_cpu[1]*self.sc_mu)\
                    and (self.sc_sched_info[0][domid] == 1\
                    and self.sc_load_info[domid] > avg_cpu_num_pool_1):
                    hunger_vms[domid] = 1
                    hunger_pool_1 += 1
            # If it's a new domain
            # 3. Move the VM to pool_0 or pool_1 (domid, pool_id)
            if hunger_vms[domid] == 1 and self.sc_ring_info[domid] == 1:
                sensitive_num += 1
                self.sc_sched_info[1][domid] = 1
            else:
                self.sc_sched_info[1][domid] = 0
        
        # if sensitive VMs is 0
        if sensitive_num == 0:
            self.sc_int_pool_num_cpu[0] = self.sc_num_cpu
            self.sc_int_pool_num_cpu[1] = 0
        else:
            # 4. Get number of CPUs of each pool
            # 4.1. Total load of none hunger of each pool
            load_non_hunger = 0.0
            load_non_hunger_0 = 0.0
            num_hunger_0 = 0
            num_hunger_1 = 0
            for domid in self.sc_load_info.keys():
                if hunger_vms[domid] == 0:
                    load_non_hunger += self.sc_load_info[domid]
                    if (self.sc_sched_info[1][domid] == 0):
                        load_non_hunger_0 += self.sc_load_info[domid]
                        num_hunger_0 += 1
                    else:
                        num_hunger_1 += 1
            # 4.2. Average cpu_num of hunger VM
            avg_cpu_num_hunger = (remain_cpus - load_non_hunger) / self.sc_num_vm
    
            # 4.3 Real CPU num of each pool
            self.sc_real_pool_num_cpu[0] = load_non_hunger_0\
                                            + (avg_cpu_num_hunger * num_hunger_0)
            self.sc_real_pool_num_cpu[1] = load_non_hunger - load_non_hunger_0\
                                            + (avg_cpu_num_hunger * num_hunger_1)
    
            # 5. Update number of cpu of each pool
            self.sc_int_pool_num_cpu[0] = int(math.floor(self.sc_real_pool_num_cpu[0]))
            self.sc_int_pool_num_cpu[1] = int(math.floor(self.sc_real_pool_num_cpu[1]))
            remainder_0 = self.sc_real_pool_num_cpu[0] - self.sc_int_pool_num_cpu[0]
            roll_0 = 1 if random.randint(0,1) <= remainder_0 else 0

            logging.debug('roll_0, ' + str(roll_0))

            roll_1 = 1 - roll_0
            self.sc_int_pool_num_cpu[0] += roll_0
            self.sc_int_pool_num_cpu[1] += roll_1
            if self.sc_int_pool_num_cpu[1] == 0:
                self.sc_int_pool_num_cpu[1] = 1
                self.sc_int_pool_num_cpu[0] -= 1

        logging.debug(str(self.sc_sched_info))
        # 6. Do pooling operations
        self.__do_pooling()

        # 7. Update sc_sched_info
        # self.sc_sched_info[0] = self.sc_sched_info[1]
        # deepcopy
        self.sc_sched_info[0] = copy.deepcopy(self.sc_sched_info[1])
        self.sc_hunger_info = copy.deepcopy(hunger_vms)
        
    # Do CPU add/remove, VM migrate between pool-0 and pool-1
    def __do_pooling(self):
        cpu_cmds = []
        migrate_cmds = []
        #cmds = []
        pool_names = ['Pool-0', 'Pool-1']
        cpupool_cpu_remove = 'xl cpupool-cpu-remove'
        cpupool_cpu_add = 'xl cpupool-cpu-add'
        cpupool_migrate = 'xl cpupool-migrate'

        # Number(integer) of cpu would be allocated to pool-1 after scheduling
        # Current number of cpu allocated to pool-1 
        difference_cpu = self.sc_int_pool_num_cpu[1] - len(self.sc_pool_info[1])
        logging.debug('self.sc_int_pool_num_cpu[1], ' +  str(self.sc_int_pool_num_cpu[1]))
        logging.debug('len(self.sc_pool_info[1]), ' + str(len(self.sc_pool_info[1])))
        logging.debug('difference_cpu, ' + str(difference_cpu))

        # If the number of CPU in the pool has changed, 
        #  remove cpu from one pool and add it to another pool.
        # 'xl cpupool-cpu-remove pool-name cpu-id'
        # 'xl cpupool-cpu-add pool-name cpu-id'

        # Pool-1 add (difference_cpu) CPUs
        if difference_cpu > 0:
            for i in range(difference_cpu):
                cpu_id = self.sc_pool_info[0].pop()
                cmd = cpupool_cpu_remove + ' ' + pool_names[0] + ' ' + str(cpu_id)
                cpu_cmds.append(cmd)
                cmd = cpupool_cpu_add + ' ' + pool_names[1] + ' ' + str(cpu_id)
                cpu_cmds.append(cmd)
                self.sc_pool_info[1].append(cpu_id)
        # Pool-1 remove (difference_cpu) CPUs
        elif difference_cpu < 0:
            for i in range(-difference_cpu):
                cpu_id = self.sc_pool_info[1].pop()
                cmd = cpupool_cpu_remove + ' ' + pool_names[1] + ' ' + str(cpu_id)
                cpu_cmds.append(cmd)
                cmd = cpupool_cpu_add + ' ' + pool_names[0] + ' ' + str(cpu_id)
                cpu_cmds.append(cmd)
                self.sc_pool_info[0].append(cpu_id)
        logging.debug('cpu_cmds, ' + str(cpu_cmds))
        # Migrate VM to it's pool
        # 'xl cpupool-migrate domain-id pool-name'
        for domid in self.sc_load_info.keys():
            if self.sc_sched_info[1][domid] != self.sc_sched_info[0][domid]:
                cmd = cpupool_migrate + ' ' + str(domid) + ' ' \
                        + pool_names[self.sc_sched_info[1][domid]]
                migrate_cmds.append(cmd)
        logging.debug('migrate_cmds, ' + str(migrate_cmds))
        # Execute cmds
        if difference_cpu < 0:
            for cmd in migrate_cmds:
                logging.debug(cmd)
                os.system(cmd)
            for cmd in cpu_cmds:
                logging.debug(cmd)
                os.system(cmd)
        elif difference_cpu == 0:
            for cmd in migrate_cmds:
                logging.debug(cmd)
                os.system(cmd)
        else:
            for cmd in cpu_cmds:
                logging.debug(cmd)
                os.system(cmd)
            for cmd in migrate_cmds:
                logging.debug(cmd)
                os.system(cmd)
